/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University. All rights reserved.
 * 
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license; 2) All redistributions in binary form
 * must reproduce the above copyright notice, this list of conditions and the
 * disclaimer listed in this license in the documentation and/or other materials
 * provided with the distribution; 3) Any documentation included with all
 * redistributions must include the following acknowledgement:
 * 
 * "This product includes software developed by the Community Grids Lab. For
 * further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/."
 * 
 * Alternatively, this acknowledgement may appear in the software itself, and
 * wherever such third-party acknowledgments normally appear.
 * 
 * 4) The name Indiana University or Community Grids Lab or NaradaBrokering,
 * shall not be used to endorse or promote products derived from this software
 * without prior written permission from Indiana University. For written
 * permission, please contact the Advanced Research and Technology Institute
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5) Products
 * derived from this software may not be called NaradaBrokering, nor may Indiana
 * University or Community Grids Lab or NaradaBrokering appear in their name,
 * without prior written permission of ARTI.
 * 
 * 
 * Indiana University provides no reassurances that the source code provided
 * does not infringe the patent or any other intellectual property rights of any
 * other entity. Indiana University disclaims any liability to any recipient for
 * claims brought by any other entity based on infringement of intellectual
 * property rights or otherwise.
 * 
 * LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO
 * WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 * NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.protocol;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import cgl.narada.util.ByteUtilities;

/**
 * <p>
 * This class keeps track of the gatekeepers that a node should be aware of.
 * This information is used by the the PPP, NOP and GPP suite of protocols. This
 * information is used to determine
 * <ul>
 * <li>The hops to reach a certain unit/super-unit.
 * <li> Enables us to conjecture when to relay events across links, since there
 * are situations when a certain unit cannot be reached.
 * <li> Detection of partitions. Failure suspectors can update this information
 * which would then allow us to deduce whether a network partition has has taken
 * place.
 * <li> Updating the gateway informations in case the subsystem this node
 * belongs to joins an existing system. This would result in updating the
 * logical addresses associated with the information stored in Gateway Info.
 * </ul>
 * The Event routing protocol should also have access to this gateway
 * informations to decide on the next unit/node that the event would be routed
 * to.
 * 
 * @author Shrideep Pallickara
 * @version : $Revision$ $Date$
 */

public class GatewayInfo implements ProtocolDebugFlags {
    /**
     * The gateway Info should be able to conjecture the routes to be taken in
     * the most simplest way.
     */
    private RoutingCache routingCache;

    /** The vertexNode within the connectivity graph */
    private Gateway vertexNode;

    /** Information regarding level l connections */
    private Vector connectionsAtLevel;

    /** The list of all the gateways in the connectivity graph */
    private Hashtable gatewayList;

    /** The number of connections (edges) within the connectivity graph */
    private int numOfConnections;

    /** The system level */
    private int systemLevel;

    /** The node Address of this node */
    private NodeAddress thisNodeAddress;

    /** The hops that can be taken from this node */
    private Vector hopsFromNode;

    /**
     * The lowest number units at each level, of the connectivity graph. This
     * information is sought by the NodeAdditionProtocol to compute the
     * destination that is best equipped to handle the node address request
     */
    private int[] lowestNumberedUnits;

    /** Protocol Handler for the node in question */
    private ProtocolHandler protocolHandler;

    /**
     * Keeps track of the number of connections at each level, that originate
     * from this node.
     */
    private int[] numOfGatewaysAtLevel;

    /**
     * Keeps track of the gateways to which profile propagations need to be
     * routed to
     */
    private Vector gatewaysAtLevel;

    /**
     * The list of destinations that this node is aware of. This is used by the
     * event routing protocol to generate the prliminary list of destinations of
     * where to route the event.
     */
    private Destinations completeDestinations;

    /** Private hashtable connectionID table */
    private Hashtable connectionTable;

    /**
     * This hashtable maintains a list of the connection edges within the
     * connectivity graph
     */
    private Hashtable connectionEdges;

    private String moduleName = "GatewayInfo: ";


    /**
     * The Gateway Info requires information regarding the system Levels that
     * are available with the system. For a system comprising only of clusters
     * the system level is 1.
     * 
     * @param systemLevel -
     *            The system level.
     */
    public GatewayInfo(int systemLevel) {
        this.systemLevel = systemLevel;
        gatewayList = new Hashtable();
        connectionsAtLevel = new Vector();
        for (int i = 0; i < systemLevel + 1; i++) {
            connectionsAtLevel.addElement(new Vector());
        }
        routingCache = new RoutingCache(this, systemLevel);
        numOfConnections = 0;
        hopsFromNode = new Vector();
        numOfGatewaysAtLevel = new int[systemLevel + 1];
        gatewaysAtLevel = new Vector();
        for (int i = 0; i < systemLevel + 1; i++) {
            gatewaysAtLevel.addElement(new Vector());
        }
        connectionTable = new Hashtable();
        connectionEdges = new Hashtable();

    }


    public GatewayInfo() {
        connectionsAtLevel = new Vector();
        hopsFromNode = new Vector();
    }


    /**
     * This method returns the system level of the system.
     * 
     * @return The system Level
     */
    public int getSystemLevel() {
        return systemLevel;
    }


    /**
     * After a node address has been assigned, this method is responsible for
     * setting the node address for the connectivity graph.
     * 
     * @param nodeAddress
     *            The node address of the server node hosting the connectivity
     *            graph
     */
    public void setNodeAddress(NodeAddress nodeAddress) {
        thisNodeAddress = nodeAddress;
        completeDestinations =
            new Destinations(nodeAddress.getAddressInBytes());
    }


    /**
     * Accessor function for retrieving the node address of the server node
     * hosting the connectivity graph.
     * 
     * @return The node address of the server node.
     */
    public NodeAddress getNodeAddress() {
        return thisNodeAddress;
    }


    /**
     * This sets the server node hosting the connectivity graph for that node,
     * as the vertex node for computations involving calculation of shortest
     * routes, detection of partitions etc.
     * 
     * @param vertexNode
     *            The vertex node for the connectivity graph.
     */
    public void setVertexNode(Gateway vertexNode) {
        this.vertexNode = vertexNode;
        if (!gatewayList.containsKey(vertexNode)) {
            gatewayList.put(vertexNode, vertexNode);
        }

        /**
         * We are simply initializing the lowest numbered units to the address
         * of the vertex Node. Once the vertex node is set, only then can
         * connections be added to the connectivity graph. Thus depending on the
         * level of the nodes contained in the connection info we update our
         * array accordingly
         */
        lowestNumberedUnits = new int[systemLevel + 1];
        byte[] nodeAddress = thisNodeAddress.getAddressInBytes();
        byte[] destValue = new byte[4];
        /**
         * We use this array for our int calculations
         */
        int offset = 0;
        for (int i = systemLevel; i >= 0; i--) {
            System.arraycopy(nodeAddress, offset, destValue, 0, 4);
            lowestNumberedUnits[i] = ByteUtilities.getInt(destValue);
            if (GatewayInfo_Debug)
                System.out.println("Lowest Numbered unit at level [" + i + "]"
                    + lowestNumberedUnits[i]);
            offset += 4;
        }
    }


    /**
     * Returns the vertex node in the connectivity graph. Useful during the
     * processing of connections for different protocol layers.
     * 
     * @return The vertex node of the connectivity graph.
     */
    public Gateway getVertexNode() {
        return vertexNode;
    }


    /**
     * This method sets the protocolHandler for the node in question. This comes
     * into the picture, to help us keep track of the kind of gateways this node
     * turns out to be, depending on the connections that are added. We use this
     * to keep track of our propagation schemes in the PPP protocol layer.
     * 
     * @param protocolHandler
     *            The protocol Handler for the node in question
     */
    public void setProtocolHandler(ProtocolHandler protocolHandler) {
        this.protocolHandler = protocolHandler;
    }


    /**
     * This method indicates if a connection already exists within the
     * connectionTable, if this is the case - the connection needs to be removed
     * from the connection table
     * 
     * @param connectionId
     *            The connection ID of the connection currently being
     *            disseminated through relevant parts of the sub-system.
     * @return <i>true</i> If the connection already exists. No further
     *         dissemination should be allowed for such connections.
     */
    public boolean containsConnection(ProtocolID connectionId) {
        if (connectionTable.containsKey(connectionId))
            return true;
        return false;
    }


    public Connection getConnection(ProtocolID connectionId) {
        Connection _connection = (Connection) connectionTable.get(connectionId);
        return _connection;
    }


    /**
     * This method adds a connection to the GatewayInfo, the connection could
     * comprise of nodes at different levels within the connectivity graph.
     * Creation of the edge in the connectivity graph, and also the process of
     * updating the routing cache in response to addition of a connection is
     * within the purview of this method.
     * 
     * @param connection -
     *            The connection that needs to be added to the connectivity
     *            graph.
     */
    public synchronized void addConnection(Connection connection) {
        ProtocolID connectionId = connection.getConnectionId();
        /**
         * This check should have already been performed prior to adding a
         * connection. Need to remove this check sometime
         */
        if (connectionTable.containsKey(connectionId))
            return;

        connectionTable.put(connectionId, connection);
        numOfConnections++;
        System.out.println("Gateway Info:-> Adding connection " + connection
            + " numOfConnections:->(" + numOfConnections + ")");

        int gatewayLevel = connection.getGatewayLevel();
        /**
         * This part deals with the creation of a connection edge in the
         * connectivity graph. If an edge already exists, the edge count simply
         * needs to be incremented. If an edge does not exist a new edge needs
         * to be created.
         */
        
        if (connectionEdgePresent(connection)) {
            incrementConnectionEdgeCount(connection);
        } else {
            addConnectionEdge(connection);
        }

        NodeAddress nodeOne, nodeTwo;
        int levelOne, levelTwo;
        nodeOne = connection.getNodeOne();
        levelOne = connection.getLevelOne();
        nodeTwo = connection.getNodeTwo();
        levelTwo = connection.getLevelTwo();

        Gateway gatewayOne, gatewayTwo;
        gatewayOne = new Gateway(vertexNode, nodeOne, (short) levelOne);

        gatewayTwo = new Gateway(vertexNode, nodeTwo, (short) levelTwo);
        

        if (!gatewayList.containsKey(gatewayOne)) {
            gatewayList.put(gatewayOne, gatewayOne);
            /**
             * We do this to maintain the list of destinations that can be
             * reached from a given node
             */
            completeDestinations.updateDestinationList(gatewayOne);
        }
        if (!gatewayList.containsKey(gatewayTwo)) {
            gatewayList.put(gatewayTwo, gatewayTwo);
            completeDestinations.updateDestinationList(gatewayTwo);
        }

        addGatewayInfo((Gateway) gatewayList.get(gatewayOne),
                       (Gateway) gatewayList.get(gatewayTwo));
        ((Vector) connectionsAtLevel.elementAt(gatewayLevel))
            .addElement(connection);
        updateRoutingCache(gatewayLevel);

        /**
         * The remainder of the method pertains to maintaining the lowest
         * numbered unit
         */
        int valOne = ByteUtilities.getInt(nodeOne.getAddressInBytes());
        int valTwo = ByteUtilities.getInt(nodeTwo.getAddressInBytes());

        if (lowestNumberedUnits[levelOne] > valOne) {
            System.out.println("Lowest numbered unit at level (" + levelOne
                + ") ->" + valOne);
            lowestNumberedUnits[levelOne] = valOne;
        }
        if (lowestNumberedUnits[levelTwo] > valTwo) {
            System.out.println("Lowest numbered unit at level (" + levelTwo
                + ") ->" + valTwo);
            lowestNumberedUnits[levelTwo] = valTwo;
        }
    }


    /**
     * In response to GPP additions of gateways. The NOP would also account for
     * the invocation of this method. This invocation represents an edge in the
     * connectivity graph.
     * 
     * @param gatewayOne -
     *            The first node in the edge.
     * @param gatewayTwo -
     *            the second node in the edge.
     */
    private void addGatewayInfo(Gateway gatewayOne, Gateway gatewayTwo) {
        if (gatewayOne.equals(vertexNode)) {
            hopsFromNode.addElement(gatewayTwo);
            if (numOfGatewaysAtLevel[gatewayTwo.getLevel()] == 0) {
                if (protocolHandler != null)
                    protocolHandler.setAsGatewayAtLevel(gatewayTwo.getLevel());
            }
            numOfGatewaysAtLevel[gatewayTwo.getLevel()]++;
            routingCache.addHop(gatewayTwo);
        }

        if (gatewayTwo.equals(vertexNode)) {
            hopsFromNode.addElement(gatewayOne);
            if (numOfGatewaysAtLevel[gatewayOne.getLevel()] == 0) {
                if (protocolHandler != null)
                    protocolHandler.setAsGatewayAtLevel(gatewayOne.getLevel());
            }
            numOfGatewaysAtLevel[gatewayOne.getLevel()]++;
            routingCache.addHop(gatewayOne);
        }
        gatewayOne.connectedToGateway(gatewayTwo);

        /**
         * This method deals with the addition of level-l gateways within the
         * level-l unit
         */
        int levelOne = gatewayOne.getLevel();
        int levelTwo = gatewayTwo.getLevel();

        if (levelOne != levelTwo) {
            if (levelOne < levelTwo) {
                // ::((Vector)gatewaysAtLevel.elementAt(levelTwo)).addElement(gatewayOne);
                // addGatewayAtLevel(gatewayOne, levelTwo);
                addGatewayAtLevel(gatewayOne, levelOne);
                addGatewayAtLevel(gatewayTwo, levelTwo);
            }

            if (levelTwo < levelOne) {
                // ::((Vector)gatewaysAtLevel.elementAt(levelOne)).addElement(gatewayTwo);
                // addGatewayAtLevel(gatewayTwo, levelOne);
                addGatewayAtLevel(gatewayOne, levelOne);
                addGatewayAtLevel(gatewayTwo, levelTwo);
            }
        }

        // ::
        if (levelOne == levelTwo) {
            addGatewayAtLevel(gatewayOne, levelOne);
            addGatewayAtLevel(gatewayTwo, levelTwo);
        }
    }


    private void addGatewayAtLevel(Gateway gateway, int level) {
        Vector _levelGateway = (Vector) gatewaysAtLevel.elementAt(level);
        if (!_levelGateway.contains(gateway)) {
            _levelGateway.addElement(gateway);
        }
    }


    /**
     * This method removes a connection from the GatewayInfo, the connection
     * could comprise of nodes at different levels within the connectivity
     * graph. Removal of the edge in the connectivity graph, and also the
     * process of updating the routing cache in response to the removal of a
     * connection is within the purview of this method.
     * 
     * @param connection -
     *            The connection that needs to be removed the connectivity
     *            graph.
     */
    public synchronized void removeConnection(Connection connection) {
        ProtocolID connectionId = connection.getConnectionId();
        /**
         * This check should have already been performed prior to adding a
         * connection. Need to remove this check sometime
         */
        if (!connectionTable.containsKey(connectionId)) {
            System.out.println(connection + "With connectionId=" + connectionId
                + " not present in list of maintained connections");
            return;
        }

        connectionTable.remove(connectionId);
        numOfConnections--;
        System.out.println("Gateway Info:-> Removing connection " + connection
            + " numOfConnections:->(" + numOfConnections + ")");

        int gatewayLevel = connection.getGatewayLevel();
        /**
         * This part deals with the removal of a connection edge in the
         * connectivity graph. If the edge count for a connection edge is 1 that
         * edge needs to be scheduled for removal. Otherwise, the edge count is
         * simply decremented and we return without removal of the edge from the
         * connectivity grah
         */
        int edgeCount = decrementConnectionEdgeCount(connection);
        if (edgeCount != 0) {
            updateRoutingCache(gatewayLevel);
            return;
        } else {
            removeConnectionEdge(connection);
        }

        NodeAddress nodeOne, nodeTwo;
        int levelOne, levelTwo;

        nodeOne = connection.getNodeOne();
        levelOne = connection.getLevelOne();
        nodeTwo = connection.getNodeTwo();
        levelTwo = connection.getLevelTwo();

        Gateway gatewayOne, gatewayTwo;
        gatewayOne = new Gateway(vertexNode, nodeOne, (short) levelOne);

        gatewayTwo = new Gateway(vertexNode, nodeTwo, (short) levelTwo);
        

        removeGatewayInfo((Gateway) gatewayList.get(gatewayOne),
                          (Gateway) gatewayList.get(gatewayTwo));

        ((Vector) connectionsAtLevel.elementAt(gatewayLevel))
            .removeElement(connection);

        updateRoutingCache(gatewayLevel);

    }


    /**
     * In response to GPP removal of gateways. The NOP would also account for
     * the invocation of this method. This invocation represents an edge in the
     * connectivity graph.
     * 
     * @param gatewayOne -
     *            The first node in the edge.
     * @param gatewayTwo -
     *            the second node in the edge.
     */
    private void removeGatewayInfo(Gateway gatewayOne, Gateway gatewayTwo) {
        if (gatewayOne.equals(vertexNode)) {
            hopsFromNode.removeElement(gatewayTwo);
            if (numOfGatewaysAtLevel[gatewayTwo.getLevel()] == 1) {
                if (protocolHandler != null)
                    protocolHandler
                        .unsetAsGatewayAtLevel(gatewayTwo.getLevel());
            }
            numOfGatewaysAtLevel[gatewayTwo.getLevel()]--;
            routingCache.removeHop(gatewayTwo);
        }

        if (gatewayTwo.equals(vertexNode)) {
            hopsFromNode.removeElement(gatewayOne);
            if (numOfGatewaysAtLevel[gatewayOne.getLevel()] == 1) {
                if (protocolHandler != null)
                    protocolHandler
                        .unsetAsGatewayAtLevel(gatewayOne.getLevel());
            }
            numOfGatewaysAtLevel[gatewayOne.getLevel()]--;
            routingCache.removeHop(gatewayOne);
        }
        gatewayOne.disconnectedFromGateway(gatewayTwo);

        /**
         * If there is a partition, for now simply remove these partioned
         * gateway nodes from the connectivity graph
         */
        if (gatewayOne.reportPartitionStatus() != 0) {
            processPartitionedGatewayNode(gatewayOne);
        }

        if (gatewayTwo.reportPartitionStatus() != 0) {
            processPartitionedGatewayNode(gatewayTwo);
        }

        /**
         * This method deals with the addition of level-l gateways within the
         * level-l unit
         */
        int levelOne = gatewayOne.getLevel();
        int levelTwo = gatewayTwo.getLevel();

        if (levelOne != levelTwo) {
            if (levelOne < levelTwo) {
                ((Vector) gatewaysAtLevel.elementAt(levelTwo))
                    .removeElement(gatewayOne);
            }

            if (levelTwo < levelOne) {
                ((Vector) gatewaysAtLevel.elementAt(levelOne))
                    .removeElement(gatewayTwo);
            }
        }
    }


    /** Deal with a partitioned node in the connectivity graph. */
    private void processPartitionedGatewayNode(Gateway gateway) {
        if (gateway.equals(vertexNode)) {
            System.out
                .println(moduleName + "Ignoring partitioning of host-node "
                    + "(vertex) " + gateway);
            return;
        }
        System.out.println(moduleName + "Processing partitioned node "
            + gateway);

        gatewayList.remove(gateway);
        completeDestinations.removeFromDestinationList(gateway);
        int gatewayLevel = gateway.getLevel();
        lowestNumberedUnits[gatewayLevel] =
            computeLowestNumberedUnitAtLevel(gatewayLevel);
        System.out.println(moduleName + "Removed " + gateway
            + " from the connectivity graph lowest numbered unit"
            + "at level (" + gatewayLevel + ") is "
            + lowestNumberedUnits[gatewayLevel]);
    }


    /**
     * This method lists the destinations at different levels that can be
     * reached from this node. This method is useful when we are trying to
     * generate the preliminary list of destinations that are associated within
     * an event.
     * 
     * @return The destinations that are known to this node.
     */
    public Destinations getListOfDestinationsKnownToNode() {
        return completeDestinations;
    }


    /**
     * This method provides us with a list of gateways at a certain level <i>l</i>
     * that exist within the level <i>l</i> units.
     * 
     * @param level
     *            This indicates the level of the gateway within a unit at that
     *            level that we are seeking.
     */
    public Vector listTheGatewaysWithinUnitAtLevel(int level) {
        if (GatewayInfo_Debug) {
            System.out.println("Listing level(" + level + ") gateways within "
                + "level(" + level + ") units ");
        }

        Vector levelGateways = (Vector) gatewaysAtLevel.elementAt(level);
        if (GatewayInfo_Debug) {
            for (Enumeration e = levelGateways.elements(); e.hasMoreElements();) {
                Gateway g = (Gateway) e.nextElement();
                System.out.println(g);
            }
            System.out.println("\n");
        }
        return levelGateways;
    }


    /**
     * When a lower level connection is established, the routing cache for
     * reaching higher level nodes is invalidated
     * 
     * @param gatewayLevel -
     *            For a connection at this level, thats added the routingCache
     *            for nodes at level >= (greater than or equal to) the
     *            gatewayLevel needs to be updated to reflect the best hop that
     *            can now be taken to reach that node.
     */
    private void updateRoutingCache(int gatewayLevel) {
        routingCache.invalidateDestinationDirectoryListings(gatewayLevel);
        int tracker = 0;
        for (Enumeration e = gatewayList.elements(); e.hasMoreElements();) {
            Gateway _gateway = (Gateway) e.nextElement();

            if (_gateway.equals(vertexNode)) {
                continue;
                /**
                 * There is no need to compute the shortest to vertex from the
                 * vertex node itself
                 */
            }
            if (_gateway.getLevel() >= gatewayLevel) {
                tracker++;
                System.out.println("Recomputing shortest path to " + _gateway
                    + " tracker->" + tracker);
                /** Add to force recalculation of the vertex path */
                _gateway.invalidateVertexPath();

                Path _path = _gateway.computeShortestPathToVertex(new Path());
                Gateway hop = _path.getBestHopToTakeToReachNode();
                System.out.println("Cost to reach " + _gateway + "is = "
                    + _path.getCost() + " over hop " + hop);
                if (hop == null) {
                    /** Indicates that _gateway is a hop */
                    System.out.println("ADDING HOP***** " + _gateway);
                    routingCache.updateRoutingInformation(_gateway, _gateway);
                } else {
                    routingCache.updateRoutingInformation(_gateway, hop);
                }
            }
        }
    }


    /**
     * Utility method which prints the Path associated with traversal to the
     * vertex node
     */
    private void printPathInformation(Gateway gateway, Path tester) {
        Gateway[] test = tester.getHopsTraversed();
        System.out.print("Shortest Path to vertex node from ("
            + gateway.getNode() + ") =>> ");
        for (int i = 0; i < test.length; i++) {
            System.out.print(test[i] + "->");
        }
        System.out.println(" Cost = " + tester.getCost() + "\n");
    }


    /**
     * In response to a sub system (LEVEL <i>level</i>) being added to an
     * existing system, the node belonging to the system needs to exachange data
     * with regarding gateways at <i>level </i> onwards in the newly merged
     * system. This method provides a view of the connectivity graph that would
     * be valid at the other node. This provides a serialized list of the
     * connections that need to be sent over a certain level
     * 
     * @param level -
     *            This is the level of the gateway over the information needs to
     *            be sent.
     * @return - The serialized representation, as a sequence of bytes, of the
     *         connections that would be consistent with the other node's view
     *         of the system.
     */
    public byte[] prepareConnectionsToSendOverLevel(int level) {
        byte[] connectionBytes;
        int serializedConnections = 0;
        int offset = 0;
        synchronized (connectionsAtLevel) {
            /* 21 bytes per connections */
            connectionBytes = new byte[numOfConnections * 21];
            for (int i = level; i < systemLevel + 1; i++) {
                Vector levelConnections =
                    (Vector) connectionsAtLevel.elementAt(i);
                for (Enumeration e = levelConnections.elements(); e
                    .hasMoreElements();) {
                    Connection c = (Connection) e.nextElement();
                    byte[] cBytes = c.snapshotConnectionInfo(level);
                    System.arraycopy(cBytes, 0, connectionBytes, offset, 21);
                    offset += 21;
                    serializedConnections++;
                }
            }
        }
        if (serializedConnections == 0) {
            /**
             * There are no nodes in the connectivity graph other than the
             * vertex node
             */
            return null;
        }

        if (serializedConnections == numOfConnections) {
            return connectionBytes;
        }

        byte[] serializedConnectionBytes = new byte[serializedConnections * 21];
        System.arraycopy(connectionBytes, 0, serializedConnectionBytes, 0,
                         serializedConnections * 21);
        return serializedConnectionBytes;
    }


    /**
     * Process the information received as a byte[] over the link. At the
     * receiving node, the connectivity graph is constructed based on the
     * information that is received at this node.
     * 
     * @param connectionBuffer
     *            The sequence of byes received over the link.
     */
    public void processConnectionBuffer(byte[] connectionBuffer) {
        /* bytes required for connection info */
        byte[] connectionBytes = new byte[21];

        int offset = 0;
        int connections = connectionBuffer.length / 21;
        for (int i = 0; i < connections; i++) {
            System.arraycopy(connectionBuffer, offset, connectionBytes, 0, 21);
            addConnection(new Connection(thisNodeAddress, connectionBytes));
            offset += 21;
        }
    }


    /**
     * This method provides a list of the direct hops that can be taken from the
     * vertex node of the connectivity graph.
     * 
     * @return - The list of hops that can be taken.
     */
    public Gateway[] getHops() {
        Gateway[] hops = new Gateway[hopsFromNode.size()];
        hopsFromNode.copyInto(hops);
        return hops;
    }


    /**
     * This method returns the best hop that needs to be taken to reach a given
     * node. This information is contained within the routing cache, and is
     * accessed by the gateway info class.
     * 
     * @param nodeToReach -
     *            Is the (node+level) gateway node that needs to be reached in
     *            the connectivity graph.
     * @return
     *            <li>The best hop that can be used to reach this node.
     *            <li> null - If no such path exists.
     */
    public Gateway getBestHopToReachNode(Gateway nodeToReach) {
        return routingCache.getBestHopToReachNode(nodeToReach);
    }


    /**
     * Returns the destinations that can be reached fastest from the specified
     * hop.
     * 
     * @param hop
     * @return
     */
    public Destinations getFastestDestinationsFromHop(Gateway hop) {
        return routingCache.getFastestDestinationsFromHop(hop);
    }


    /**
     * Provides the destinations that can be reached from a hop. The
     * destinations are returned as a node address.
     * 
     * @param gateway -
     *            The hop from the server node hosting the connectivity graph.
     * @return The destinations that can be reached from this hop, if it is a
     *         valid hop. Otherwise return null.
     * 
     */
    public Destinations getDestinationsReachedFromHop(Gateway gateway) {
        return routingCache.destinationsReachedFromHop(gateway);
    }


    /**
     * When presented within a list of destinations that have already been
     * traversed, this method returns a list of hops that can be taken in a
     * manner consistent with our policy of <i> not routing an event to the same
     * node twice.</i>
     * 
     * @param routingInformation -
     *            The routing information contained within a message i.e. event,
     *            protocol packets etc.
     * @return - The list of valid hops that can be taken
     */
    public Gateway[] getListOfValidHopsToRoute(byte[] routingInformation) {
        for (Enumeration e = hopsFromNode.elements(); e.hasMoreElements();) {
            Gateway gateway = (Gateway) e.nextElement();
            if (gateway.containedInRoutingInformation(routingInformation)) {
                System.out.println("Gateway: " + gateway + " can be used ");
            }
        }
        return null;
    }


    /**
     * A test function which enumerates the hops that need to be taken to reach
     * any node in the connectivity graph.
     */
    public void enumerateBestHopsToTake() {
        for (Enumeration e = gatewayList.elements(); e.hasMoreElements();) {
            Gateway g = (Gateway) e.nextElement();
            System.out.println("Best hop to reach " + g + " =>"
                + routingCache.getBestHopToReachNode(g));
        }
        routingCache.performDestinationDirectoryListing();
    }


    /**
     * React to failure suspicions, update the gateway info available and
     * compute new routes to reach destinations
     * 
     * @param level
     *            The level of the node that failed
     * @param gateway
     *            The gateway node in the graph.
     */
    public void removeGatewayInfo(Short level, Gateway gateway) {
    }


    /**
     * This method provides a list of links over which a certain event must be
     * sent to reach the destinations computed by the profile matching and
     * contained within the event's routing information.
     */
    public Gateway[] hopsToReachDestination(Destinations destinationToReach,
                                            Destinations traversedSoFar) {

        // return routingCache.hopsToReachDestination(destinationToReach,
        // traversedSoFar);
        return routingCache.computeHopsToReachDestination(destinationToReach,
                                                          traversedSoFar);
    }


    /**
     * This method provides us with the lowest numbered unit at a certain level
     * within the connectivity graph. This is used by the node addition protocol
     * to compute the destination lists associated with a node addition address
     * request
     * 
     * @param level
     *            The level for which the lowest numbered unit is sought.
     */
    public int getLowestNumberedUnitAtLevel(int level) {
        if (level > systemLevel) {
            System.out.println(moduleName
                + "Malformed lowestNumberedUnit access ");
        }
        return lowestNumberedUnits[level];
    }


    /**
     * A utility method that allows us to compute the lowested numbered unit at
     * a given level
     */
    public int computeLowestNumberedUnitAtLevel(int level) {
        int nodesAtLevel = completeDestinations.getDestinationsAtLevel(level);
        int lowestPosition = ByteUtilities.getFirstOneBitPosition(nodesAtLevel);

        int lowestUnit = 0;
        if (lowestPosition > 0) {
            lowestUnit = 1;
            lowestUnit = lowestUnit << (lowestPosition - 1);
        }

        if (lowestUnit == 0) {
            System.out.println(moduleName + "Lowest Position = 0. INVALID!!!");
        }

        return lowestUnit;
    }


    /**
     * When this method returns <i>true</i> an edge has already been
     * established in the connectivity graph. There is therefore no need to
     * create another edge for the same connection.
     * 
     * @return <i>true</i> if the edge is already present, <i>false</i> if it
     *         is not
     */
    private boolean connectionEdgePresent(Connection connection) {
        if (connectionEdges.containsKey(connection)) {
            return true;
        }
        return false;
    }


    /**
     * Increment the edge count associated with a connection edge. This should
     * be invoked if the <i> connectionEdgePresent()</i> method returns <i>true</i>.
     * 
     * @return edgeCount The new count associated with this connection edge.
     */
    private int incrementConnectionEdgeCount(Connection connection) {
        int edgeCount = ((Integer) connectionEdges.get(connection)).intValue();
        edgeCount++;
        connectionEdges.put(connection, new Integer(edgeCount));
        System.out.println("Edge count for connection [" + connection + "] = "
            + edgeCount);
        return edgeCount;
    }/* end increment edge count */


    /** Return false if the operation was not successful */
    private boolean addConnectionEdge(Connection connection) {
        if (!connectionEdgePresent(connection)) {
            connectionEdges.put(connection, new Integer(1));
            return true;
        }
        System.out.println("Trying to add existing connection edge"
            + connection);
        return false;
    }


    /**
     * Increment the edge count associated with a connection edge. This should
     * be invoked if the <i> connectionEdgePresent()</i> method returns <i>true</i>.
     * 
     * @return edgeCount The new count associated with this connection edge.
     */
    private int decrementConnectionEdgeCount(Connection connection) {
        int edgeCount = ((Integer) connectionEdges.get(connection)).intValue();
        edgeCount--;
        connectionEdges.put(connection, new Integer(edgeCount));
        System.out.println("Edge count for connection [" + connection
            + "] reduced to => " + edgeCount);
        return edgeCount;
    }/* end increment edge count */


    /**
     * Return false if the operation was invoked on a connection edge whose edge
     * count was greater than 1.
     */
    private boolean removeConnectionEdge(Connection connection) {
        /*
         * This method should be invoked only after the edge count associated
         * with the destination had been reduced to zero.
         */
        int edgeCount = ((Integer) connectionEdges.get(connection)).intValue();
        if (edgeCount == 0) {
            connectionEdges.remove(connection);
            return true;
        }
        System.out
            .println("Removing a connection edge where the edge count for "
                + connection + " = " + edgeCount);
        connectionEdges.remove(connection);
        return false;
    }


    public static void main(String[] args) {
        System.out.println("Tesing => cgl.narada.protocol.GatewayInfo ");

        /** Testing the connectivity graph */
        int[] a6 = { 1 };
        int[] a4 = { 2 };
        int[] a5 = { 3 };
        int[] a7 = { 4 };

        int[] aa = { 7 };
        int[] ab = { 8 };
        int[] ac = { 9 };
        int[] aSC_2 = { 10 };
        int[] aSC_3 = { 11 };
        int[] aSSC_B = { 20 };
        int[] aSSC_C = { 21 };
        int[] aSSC_D = { 22 };
        NodeAddress na6, na5, na4, na7, naa, nab, naSC_3, naSC_2, naSSC_B, naSSC_C, naSSC_D;
        na6 = new NodeAddress(a6);
        na5 = new NodeAddress(a5);
        na4 = new NodeAddress(a4);
        na7 = new NodeAddress(a7);

        naa = new NodeAddress(aa);
        nab = new NodeAddress(ab);

        naSC_2 = new NodeAddress(aSC_2);
        naSC_3 = new NodeAddress(aSC_3);

        naSSC_B = new NodeAddress(aSSC_B);
        naSSC_C = new NodeAddress(aSSC_C);
        naSSC_D = new NodeAddress(aSSC_D);

        int[] addressOfNode = { 1, 1, 1, 1 };
        NodeAddress thisNodeAddress = new NodeAddress(addressOfNode);
        ProtocolIDFactory idFactory =
            new ProtocolIDFactory(thisNodeAddress, (short) 0);

        Connection c64 =
            new Connection(thisNodeAddress, na6, 0, na4, 0, 0, idFactory
                .getProtocolID());
        Connection c65 =
            new Connection(thisNodeAddress, na6, 0, na5, 0, 0, idFactory
                .getProtocolID());
        Connection c67 =
            new Connection(thisNodeAddress, na7, 0, na6, 0, 0, idFactory
                .getProtocolID());

        Connection c4a =
            new Connection(thisNodeAddress, na4, 0, naa, 1, 1, idFactory
                .getProtocolID());
        Connection cab =
            new Connection(thisNodeAddress, naa, 1, nab, 1, 1, idFactory
                .getProtocolID());

        Connection cbSC_3 =
            new Connection(thisNodeAddress, nab, 1, naSC_3, 2, 2, idFactory
                .getProtocolID());
        Connection c5SC_2 =
            new Connection(thisNodeAddress, na5, 0, naSC_2, 2, 2, idFactory
                .getProtocolID());
        Connection cSC_3SC_2 =
            new Connection(thisNodeAddress, naSC_2, 2, naSC_3, 2, 2, idFactory
                .getProtocolID());

        Connection caSSC_B =
            new Connection(thisNodeAddress, naa, 1, naSSC_B, 3, 3, idFactory
                .getProtocolID());
        Connection cSC_3SSC_B =
            new Connection(thisNodeAddress, naSC_3, 2, naSSC_B, 3, 3, idFactory
                .getProtocolID());
        Connection cSSC_BSSC_D =
            new Connection(thisNodeAddress, naSSC_B, 3, naSSC_D, 3, 3,
                           idFactory.getProtocolID());
        Connection cSSC_CSSC_D =
            new Connection(thisNodeAddress, naSSC_C, 3, naSSC_D, 3, 3,
                           idFactory.getProtocolID());
        Connection cSC_2SSC_C =
            new Connection(thisNodeAddress, naSC_2, 2, naSSC_C, 3, 3, idFactory
                .getProtocolID());

        Connection c7SC_3 =
            new Connection(thisNodeAddress, na7, 0, naSC_3, 2, 2, idFactory
                .getProtocolID());

        GatewayInfo gatewayInfo = new GatewayInfo(3);
        gatewayInfo.setNodeAddress(thisNodeAddress);
        gatewayInfo.setVertexNode(new Gateway(na6, (short) 0, true));

        gatewayInfo.addConnection(c64);
        gatewayInfo.addConnection(c65);
        gatewayInfo.addConnection(c67);
        gatewayInfo.addConnection(c4a);
        gatewayInfo.addConnection(cab);
        gatewayInfo.addConnection(cbSC_3);
        gatewayInfo.addConnection(c5SC_2);
        gatewayInfo.addConnection(cSC_3SC_2);
        gatewayInfo.addConnection(caSSC_B);
        gatewayInfo.addConnection(cSC_3SSC_B);
        gatewayInfo.addConnection(cSSC_BSSC_D);
        gatewayInfo.addConnection(cSSC_CSSC_D);
        gatewayInfo.addConnection(cSC_2SSC_C);

        /** Testing the cache update behavior */
        System.out.println("=> " + "\n");
        gatewayInfo.addConnection(c7SC_3);

        gatewayInfo.listTheGatewaysWithinUnitAtLevel(1);
        gatewayInfo.listTheGatewaysWithinUnitAtLevel(2);
        gatewayInfo.listTheGatewaysWithinUnitAtLevel(3);

        /*
         * Gateway gateway = gSSC_D; Path tester =
         * gateway.computeShortestPathToVertex(new Path()); Gateway[] test =
         * tester.getHopsTraversed(); System.out.print("Shortest Path to node
         * (6) from (" + gateway.getNode() + ") =>> "); for (int i=0; i <
         * test.length; i++) { System.out.print(test[i] + "->" ); }
         * System.out.println("Total Cost =" + tester.getCost());
         * gSSC_C.computeShortestPathToVertex(new Path());
         * gSSC_D.computeShortestPathToVertex(new Path());
         */

        /** Testing the hops that exist from the hosting server node */
        Gateway[] hops = gatewayInfo.getHops();
        for (int i = 0; i < hops.length; i++) {
            System.out.println("Gateway hop ->" + hops[i]);
        }

        /**
         * Testing the list of valid hops that need to be taken given a certain
         * routing information
         */
        byte[] routingInfo = new byte[16];
        routingInfo[15] = (byte) 4;
        gatewayInfo.getListOfValidHopsToRoute(routingInfo);

        /** Enumerating the routing cache contents */
        gatewayInfo.enumerateBestHopsToTake();

        System.gc();
        System.out.println("\n" + "Testing connections marhsall/unmarshall"
            + "\n");
        GatewayInfo gatewayInfoTwo = new GatewayInfo(3);
        gatewayInfoTwo.setNodeAddress(thisNodeAddress);
        gatewayInfoTwo.setVertexNode(new Gateway(na6, (short) 0, true));
        byte[] connByteBuffer =
            gatewayInfo.prepareConnectionsToSendOverLevel(0);
        gatewayInfoTwo.processConnectionBuffer(connByteBuffer);
        gatewayInfoTwo.addConnection(new Connection(thisNodeAddress, na5, 0,
                                                    na4, 0, 0, idFactory
                                                        .getProtocolID()));
    }/* end-main */

}
